Skip to main content

Proxy Headers

When NGINX works as a reverse proxy, it sits between the client and the backend server.

Client ──► NGINX (proxy) ──► Backend

Without proxy headers, the backend only sees:

  • NGINX’s IP address
  • NGINX’s protocol (http/https)
  • NGINX as the direct client

X-Forwarded-* headers preserve original client information.

Why Proxy Headers Are Needed

They allow backend applications to know:

  • Real client IP address
  • Original host/domain requested
  • Original protocol (HTTP or HTTPS)
  • Proxy chain (multiple proxies)

Used for:

  • Logging
  • Security rules
  • Authentication
  • Redirects
  • Geo/IP detection
  • Rate limiting

Core X-Forwarded-* Headers

X-Forwarded-For

  • Purpose: Identifies the original client IP address.
  • Format X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip
  • NGINX Directive: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Example Flow

Client IP: 203.0.113.10

X-Forwarded-For: 203.0.113.10

With multiple proxies:

X-Forwarded-For: 203.0.113.10, 192.168.1.1

Backend Use

  • Detect real client IP
  • Prevent abuse
  • Accurate logging

X-Real-IP

  • Purpose: Provides a single client IP address (simpler than X-Forwarded-For).
  • NGINX Directive: proxy_set_header X-Real-IP $remote_addr;

Example

X-Real-IP: 203.0.113.10

Difference from X-Forwarded-For

HeaderUse
X-Real-IPSimple, one IP
X-Forwarded-ForFull proxy chain

X-Forwarded-Proto

  • Purpose: Tells the backend the original protocol used by the client.
  • Values: http, https
  • NGINX Directive: proxy_set_header X-Forwarded-Proto $scheme;

Example

Client accesses:

https://example.com

Backend receives:

X-Forwarded-Proto: https

Why It Matters

  • Correct redirects
  • OAuth callbacks
  • Secure cookies
  • HTTPS enforcement

X-Forwarded-Host

  • Purpose: Contains the original Host header requested by the client.
  • NGINX Directive: proxy_set_header X-Forwarded-Host $host;

Example

X-Forwarded-Host: example.com

X-Forwarded-Port

  • Purpose: Indicates the original port used by the client.
  • NGINX Directive: proxy_set_header X-Forwarded-Port $server_port;

Example

X-Forwarded-Port: 443

The Host Header (Important!)

Although not X-Forwarded-\*, it is critical.

proxy_set_header Host $host;
  • Preserves original domain name
  • Required for:
    • Virtual hosting
    • TLS SNI
    • Correct routing
location / {
proxy_pass http://backend;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;

}

This is considered best practice.

End-to-End Request Example

Client Request

GET /login HTTP/1.1
Host: example.com

Headers Sent to Backend

Host: example.com
X-Real-IP: 203.0.113.10
X-Forwarded-For: 203.0.113.10
X-Forwarded-Proto: https
X-Forwarded-Host: example.com
X-Forwarded-Port: 443

Application-Level Usage

Example: Backend Redirect Logic

if request.headers["X-Forwarded-Proto"] == "https":
secure = True

Example: Logging Real IP

$remote_addr → NGINX IP
X-Forwarded-For → Client IP

Security Considerations

These headers can be spoofed by clients.

Best Practices

  • Only trust headers from known proxies
  • Use NGINX real_ip module to sanitize
  • Strip incoming X-Forwarded-\* headers
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

(Overwrites client-sent value)

X-Forwarded-\* vs Forwarded (RFC 7239)

Modern Alternative

Forwarded: for=203.0.113.10;proto=https;host=example.com

NGINX supports it, but:

  • X-Forwarded-\* is still more widely used
  • Most frameworks expect X-Forwarded-\*

Common Mistakes

  • Forgetting X-Forwarded-Proto (causes redirect loops)
  • Trusting client-provided headers
  • Missing Host header
  • Using $remote_addr incorrectly

Real-World SSL Termination Example

server {
listen 443 ssl;

location / {
proxy_pass http://app_backend;

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

}

Backend runs HTTP but behaves as HTTPS-aware.